#!/usr/bin/python
# 
# Copyright (C) 2009 Frank Ronny Larsen.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys
import gtk
import gnome.ui
import gobject
import pango

from snooper import SnoopThread
from httpchat import HTTP_Chat
from hextext import hextext

columnspecs = [{'title': "Time", "text_column": 1},
               {'title': "Method", "text_column": 2},
               {'title': "URL", "text_column": 3,
                'properties': { 'expand': True },
                'render_properties': { 'ellipsize': pango.ELLIPSIZE_END}
                },
               {'title': "Result", "text_column": 4},
               {'title': "Size", "text_column": 5},
               {'title': "Content type", "text_column": 6}]

header_columns = [{'title': "Header", "text_column": 0,
                   'render_properties': { 'weight': 800, 'foreground': "blue"}},
                  {'title': "Value", "text_column": 1}]

class PacketListStore(gtk.ListStore):
    """A specialized gtk.ListStore for HTTP packet data"""
    def __init__(self):
        super(PacketListStore, self).__init__(object, str, str, str, str, str, str)

    def append_packet(self, chat, index):
        obj = chat.objects[index]
        a = [obj, obj.time_string, obj.method, obj.url, obj.result, str(obj.size), obj.ctype]
        print "appending: ", a
        giter = self.append(a)
        obj.giter = giter
        # Always return false since function is used as gobject.idle_add
        return False

    def update_packet(self, obj):
        a = []
        self.set(obj.giter, 2, obj.method, 3, obj.url, 4, obj.result, 5, str(obj.size), 6, obj.ctype)
        # Always return false since function is used as gobject.idle_add
        return False

class DetailsWindow(object):
    def __init__(self, builder, chat):
        builder = gtk.Builder()
        builder.add_from_file(res_file("hottipi.xml"))
        self.window = builder.get_object("windowChatDetails")
        self.notebook = builder.get_object("nbDetails")
        builder.connect_signals(self)
        self.builder = builder

        self.chat = chat

        if self.chat.request.method not in ["POST", "PUT"]:
            # Integer index. How smart... This will become wrong if
            # ordering of pages is altered in the glade/builder file.
            self.notebook.remove_page(2)

    def on_tabSummary_show(self, obj):
        print "SHOWING SUMMARY", [obj]

        reqline = self.builder.get_object("entryRequestLine")
        rspline = self.builder.get_object("entryResponseLine")
        ctype = self.builder.get_object("entryContentType")
        reqsize = self.builder.get_object("entryRequestSize")
        rspsize = self.builder.get_object("entryResponseSize")

        reqline.set_text(self.chat.request.line)
        rspline.set_text(self.chat.response.line)
        ctype.set_text(self.chat.ctype)
        reqsize.set_text(str(self.chat.request.size))
        rspsize.set_text(str(self.chat.response.size))
    
    def on_tabHeaders_show(self, obj):
        tvwRequest = self.builder.get_object("tvwRequest")
        tvwResponse = self.builder.get_object("tvwResponse")

        reqliststore = gtk.ListStore(str,str)
        tvwRequest.set_model(reqliststore)
        rspliststore = gtk.ListStore(str,str)
        tvwResponse.set_model(rspliststore)

        for col in header_columns:
            renderer = gtk.CellRendererText()
            for prop,val in col.get("render_properties", dict()).items():
                renderer.set_property(prop, val)
            gcol = gtk.TreeViewColumn(col['title'], renderer, text=col['text_column'])
            for prop,val in col.get("properties", dict()).items():
                gcol.set_property(prop, val)
            tvwRequest.append_column(gcol)
        for col in header_columns:
            renderer = gtk.CellRendererText()
            for prop,val in col.get("render_properties", dict()).items():
                renderer.set_property(prop, val)
            gcol = gtk.TreeViewColumn(col['title'], renderer, text=col['text_column'])
            for prop,val in col.get("properties", dict()).items():
                gcol.set_property(prop, val)
            tvwResponse.append_column(gcol)

        for key,val in self.chat.request.headers.items():
            reqliststore.append([key,val])
        for key,val in self.chat.response.headers.items():
            rspliststore.append([key,val])

    def on_tabPostdata_show(self, obj):
        viewportPostdata = self.builder.get_object("viewportPostdata")

        widget = gtk.TextView()
        buffer = gtk.TextBuffer()
        buffer.set_text(self.chat.request.body)
        widget.set_buffer(buffer)

        viewportPostdata.add(widget)
        viewportPostdata.show_all()

    def on_tabResponse_show(self, obj):
        text_types = [ "application/x-javascript" ]
        viewportResponse = self.builder.get_object("viewportResponse")

        # Determine ctype.
        ctype = self.chat.response.headers['Content-Type']
        widget = None
        if ctype.startswith("image"):
            # Create an image widget
            # FIXME: Currently just a hex view.
            # TODO: Find a way of loading an image into gtk.Image
            # without using temporary files.
            widget = gtk.TextView()
            buffer = gtk.TextBuffer()
            buffer.set_text(hextext(self.chat.response.body))
            widget.set_buffer(buffer)
            widget.modify_font(pango.FontDescription("Monospace"))
            
        elif ctype.startswith("text") or ctype in text_types:
            # Create a text view
            widget = gtk.TextView()
            buffer = gtk.TextBuffer()
            buffer.set_text(self.chat.response.body)
            widget.set_buffer(buffer)
        else:
            # Create a hexdump view.
            widget = gtk.TextView()
            buffer = gtk.TextBuffer()
            buffer.set_text(hextext(self.chat.response.body))
            widget.set_buffer(buffer)
            widget.modify_font(pango.FontDescription("Monospace"))

        # Create ctype container
        # Add to viewport
        if widget:
            viewportResponse.add(widget)
            viewportResponse.show_all()

    def on_tbtnDetailsClose_clicked(self, obj):
        self.window.destroy()

class HotTipiMain(object):
    def __init__(self, builder, filename=None):
        # Get UI elements we need to use from the GTK builder.
        self.builder = builder
        self.window = builder.get_object("windowMain")
        self.dialogPreferences = builder.get_object("dialogPreferences")
        self.dialogAbout = builder.get_object("dialogAbout")
        self.dialogOpen = builder.get_object("dialogOpen")
        self.appbar = builder.get_object("statusbar1")
        self.tbtnSnooping = builder.get_object("tbtnSnooping")
        
        self.snoopthread = None

        builder.connect_signals(self)

        self.tvwConnections = builder.get_object("tvwConnections")
        self.liststore = PacketListStore()
        self.tvwConnections.set_model(self.liststore)

        #First column's cell
        for col in columnspecs:
            renderer = gtk.CellRendererText()
            for prop,val in col.get("render_properties", dict()).items():
                renderer.set_property(prop, val)
            gcol = gtk.TreeViewColumn(col['title'], renderer, text=col['text_column'])
            for prop,val in col.get("properties", dict()).items():
                gcol.set_property(prop, val)
            self.tvwConnections.append_column(gcol)

        self.filename = filename
        # Trigger parsing snoopthread
        if self.filename:
            self.tbtnSnooping.set_active(True)

        self.appbar.push(1, "Application loaded.")
        

    def on_menuAbout_activate(self, widget, data=None):
        self.dialogAbout.show()

    def on_dialogAbout_close(self, widget, data=None):
        self.dialogAbout.hide()

    def on_menuPreferences_activate(self, widget, data=None):
        # Update all preferences fields.
        self.dialogPreferences.show()

    def on_dialogPreferences_close(self, widget, data=None):
        self.dialogPreferences.hide()

    def on_windowMain_destroy(self, widget, data=None):
        if self.snoopthread:
            self.snoopthread.quit()
            self.snoopthread = None
        gtk.main_quit()

    def on_menuOpen_activate(self, widget, data=None):
        # Update all preferences fields.
        self.dialogOpen.show()

    def on_btnDialogOpenCancel_clicked(self, widget, data=None):
        print "Cancel file search!"
        self.dialogOpen.hide()

    def load_file_from_dialog(self, widget, data=None):
        print "File Activated!!"
        filename = self.dialogOpen.get_filename()
        self.dialogOpen.hide()

        self.filename = filename
        self.tbtnSnooping.set_active(True)
        self.appbar.push(1, "Loading data from file...")

    def clear_list(self, widget, data=None):
        print "Clear list please"
        if self.snoopthread:
            self.snoopthread.quit()
            self.snoopthread = None

        self.liststore.clear()

    def on_tvwConnections_button_press_event(self, widget, event):
        if event.type != gtk.gdk._2BUTTON_PRESS:
            return
        
        sel = widget.get_selection()
        model, itr = sel.get_selected()
        if not itr: return # Click outside data

        chat = model.get_value(itr, 0)
        wnd = DetailsWindow(self.builder, chat)
        wnd.window.show()

    def on_tbtnSnooping_toggled(self, widget, data=None):
        # Stop running thread
        if self.snoopthread:
            self.snoopthread.quit()
            self.snoopthread = None

        # Start new thread if toggled on.
        if widget.get_active():
            self.snoopthread = SnoopThread(self.liststore, filename=self.filename, main=self)
            self.snoopthread.start()
            if not self.filename:
                self.appbar.push(1, "Listening for data...")
            self.filename = None

    def snooperQuit(self, success, data=None):
        self.snoopthread = None
        if success:
            self.appbar.push(1, "Snoop thread exited.")
        else:
            # Popup alert
            self.appbar.push(1, "Snoop thread exited because of error. Live snooping requires root privileges.")

        self.tbtnSnooping.set_active(False)


def res_file(name):
    return "../resources/" + name

if __name__ == "__main__":
    builder = gtk.Builder()
    builder.add_from_file(res_file("hottipi.xml"))

    infile = None
    if len(sys.argv) > 1:
        infile = sys.argv[1]

    main = HotTipiMain(builder, filename=infile)
    main.window.show()
    gobject.threads_init()

    gnome.program_init("Hot Tipi", "0.0.2")
    gtk.main()

    # If we get here we want to quit the snoop as well
    if main.snoopthread:
        main.snoopthread.quit()
